/*
 *	This file is part of Transdroid <http://www.transdroid.org>
 *
 *	Transdroid is free software: you can redistribute it and/or modify
 *	it under the terms of the GNU General Public License as published by
 *	the Free Software Foundation, either version 3 of the License, or
 *	(at your option) any later version.
 *
 *	Transdroid is distributed in the hope that it will be useful,
 *	but WITHOUT ANY WARRANTY; without even the implied warranty of
 *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *	GNU General Public License for more details.
 *
 *	You should have received a copy of the GNU General Public License
 *	along with Transdroid.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package org.transdroid.daemon.adapters.rTorrent;

import android.text.TextUtils;

import org.transdroid.core.gui.log.Log;
import org.transdroid.daemon.Daemon;
import org.transdroid.daemon.DaemonException;
import org.transdroid.daemon.DaemonException.ExceptionType;
import org.transdroid.daemon.DaemonSettings;
import org.transdroid.daemon.IDaemonAdapter;
import org.transdroid.daemon.Label;
import org.transdroid.daemon.Priority;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentDetails;
import org.transdroid.daemon.TorrentFile;
import org.transdroid.daemon.TorrentStatus;
import org.transdroid.daemon.task.AddByFileTask;
import org.transdroid.daemon.task.AddByMagnetUrlTask;
import org.transdroid.daemon.task.AddByUrlTask;
import org.transdroid.daemon.task.DaemonTask;
import org.transdroid.daemon.task.DaemonTaskFailureResult;
import org.transdroid.daemon.task.DaemonTaskResult;
import org.transdroid.daemon.task.DaemonTaskSuccessResult;
import org.transdroid.daemon.task.GetFileListTask;
import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
import org.transdroid.daemon.task.GetTorrentDetailsTask;
import org.transdroid.daemon.task.GetTorrentDetailsTaskSuccessResult;
import org.transdroid.daemon.task.RemoveTask;
import org.transdroid.daemon.task.RetrieveTask;
import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
import org.transdroid.daemon.task.SetFilePriorityTask;
import org.transdroid.daemon.task.SetLabelTask;
import org.transdroid.daemon.task.SetTransferRatesTask;
import org.transdroid.daemon.util.HttpHelper;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import de.timroes.axmlrpc.XMLRPCClient;
import de.timroes.axmlrpc.XMLRPCClient.UnauthorizdException;
import de.timroes.axmlrpc.XMLRPCException;

/**
 * An adapter that allows for easy access to rTorrent torrent data. Communication is handled via the XML-RPC protocol as
 * implemented by the aXMLRPC library.
 *
 * @author erickok
 */
public class RTorrentAdapter implements IDaemonAdapter {

    private static final String LOG_NAME = "rTorrent daemon";

    private static final String DEFAULT_RPC_URL = "/RPC2";
    private static final int XMLRPC_MINIMUM_SIZE = 2 * 1024 * 1024;
    private static final int XMLRPC_EXTRA_PADDING = 1280;

    private DaemonSettings settings;
    private XMLRPCClient rpcclient;
    private List<Label> lastKnownLabels = null;
    private Integer version = null;

    public RTorrentAdapter(DaemonSettings settings) {
        this.settings = settings;
    }

    @Override
    public DaemonTaskResult executeTask(Log log, DaemonTask task) {

        try {
            // Ensure a version number is know to switch to the right methods
            if (version == null) {
                try {
                    Object versionObject = makeRtorrentCall(log, "system.client_version", new String[0]);
                    String[] versionRaw = versionObject.toString().split("\\.");
                    version = (Integer.parseInt(versionRaw[0]) * 10000) + (Integer.parseInt(versionRaw[1]) * 100) + Integer.parseInt(versionRaw[2]);
                } catch (Exception e) {
                    version = 10000;
                }
            }
            switch (task.getMethod()) {
                case Retrieve:

                    // @formatter:off
                    Object result = makeRtorrentCall(log, "d.multicall2",
                            new String[]{"", "main",
                                    "d.hash=",
                                    "d.name=",
                                    "d.state=",
                                    "d.down.rate=",
                                    "d.up.rate=",
                                    "d.peers_connected=",
                                    "d.peers_not_connected=",
                                    "d.peers_accounted=",
                                    "d.bytes_done=",
                                    "d.up.total=",
                                    "d.size_bytes=",
                                    "d.creation_date=",
                                    "d.left_bytes=",
                                    "d.complete=",
                                    "d.is_active=",
                                    "d.is_hash_checking=",
                                    "d.is_multi_file=",
                                    "d.base_filename=",
                                    "d.message=",
                                    "d.custom=addtime",
                                    "d.custom=seedingtime",
                                    "d.custom1=",
                                    "d.peers_complete=",
                                    "d.peers_accounted=",
                                    "d.is_open="});
                    // @formatter:on
                    return new RetrieveTaskSuccessResult((RetrieveTask) task, onTorrentsRetrieved(result),
                            lastKnownLabels);

                case GetTorrentDetails:

                    // @formatter:off
                    Object dresult = makeRtorrentCall(log, "t.multicall", new String[]{
                            task.getTargetTorrent().getUniqueID(),
                            "",
                            "t.url="});
                    // @formatter:on
                    return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task,
                            onTorrentDetailsRetrieved(log, dresult));

                case GetFileList:

                    // @formatter:off
                    Object fresult = makeRtorrentCall(log, "f.multicall", new String[]{
                            task.getTargetTorrent().getUniqueID(),
                            "",
                            "f.path=",
                            "f.size_bytes=",
                            "f.priority=",
                            "f.completed_chunks=",
                            "f.size_chunks=",
                            "f.priority=",
                            "f.frozen_path="});
                    // @formatter:on
                    return new GetFileListTaskSuccessResult((GetFileListTask) task,
                            onTorrentFilesRetrieved(fresult, task.getTargetTorrent()));

                case AddByFile:

                    // Request to add a torrent by local .torrent file
                    File file = new File(URI.create(((AddByFileTask) task).getFile()));
                    FileInputStream in = new FileInputStream(file);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    byte[] buffer = new byte[(int) file.length()];
                    int read;
                    while ((read = in.read(buffer, 0, buffer.length)) > 0) {
                        baos.write(buffer, 0, read);
                    }
                    byte[] bytes = baos.toByteArray();
                    int size = Math.max(((int) file.length() * 2) + XMLRPC_EXTRA_PADDING, XMLRPC_MINIMUM_SIZE);
                    if (version >= 904) {
                        makeRtorrentCall(log, "network.xmlrpc.size_limit.set", new Object[]{"", size + XMLRPC_EXTRA_PADDING});
                        makeRtorrentCall(log, "load.raw_start", new Object[]{"", bytes});
                    } else {
                        makeRtorrentCall(log, "set_xmlrpc_size_limit", new Object[]{size + XMLRPC_EXTRA_PADDING});
                        makeRtorrentCall(log, "load_raw_start", new Object[]{bytes});
                    }
                    return new DaemonTaskSuccessResult(task);

                case AddByUrl:

                    // Request to add a torrent by URL
                    String url = ((AddByUrlTask) task).getUrl();
                    if (version >= 904) {
                        makeRtorrentCall(log, "load.start", new String[]{"", url});
                    } else {
                        makeRtorrentCall(log, "load_start", new String[]{url});
                    }
                    return new DaemonTaskSuccessResult(task);

                case AddByMagnetUrl:

                    // Request to add a magnet link by URL
                    String magnet = ((AddByMagnetUrlTask) task).getUrl();
                    if (version >= 904) {
                        makeRtorrentCall(log, "load.start", new String[]{"", magnet});
                    } else {
                        makeRtorrentCall(log, "load_start", new String[]{magnet});
                    }
                    return new DaemonTaskSuccessResult(task);

                case Remove:

                    // Remove a torrent
                    RemoveTask removeTask = (RemoveTask) task;
                    if (removeTask.includingData()) {
                        makeRtorrentCall(log, "d.custom5.set",
                                new String[]{task.getTargetTorrent().getUniqueID(), "1"});
                    }
                    makeRtorrentCall(log, "d.erase", new String[]{task.getTargetTorrent().getUniqueID()});
                    return new DaemonTaskSuccessResult(task);

                case Pause:

                    // Pause a torrent
                    makeRtorrentCall(log, "d.stop", new String[]{task.getTargetTorrent().getUniqueID()});
                    return new DaemonTaskSuccessResult(task);

                case PauseAll:

                    // Resume all torrents
                    makeRtorrentCall(log, "d.multicall2", new String[]{"", "main", "d.stop="});
                    return new DaemonTaskSuccessResult(task);

                case Resume:

                    // Resume a torrent
                    makeRtorrentCall(log, "d.start", new String[]{task.getTargetTorrent().getUniqueID()});
                    return new DaemonTaskSuccessResult(task);

                case ResumeAll:

                    // Resume all torrents
                    makeRtorrentCall(log, "d.multicall2", new String[]{"", "main", "d.start="});
                    return new DaemonTaskSuccessResult(task);

                case Stop:

                    // Stop a torrent
                    makeRtorrentCall(log, "d.stop", new String[]{task.getTargetTorrent().getUniqueID()});
                    makeRtorrentCall(log, "d.close", new String[]{task.getTargetTorrent().getUniqueID()});
                    return new DaemonTaskSuccessResult(task);

                case StopAll:

                    // Stop all torrents
                    makeRtorrentCall(log, "d.multicall2", new String[]{"", "main", "d.stop=", "d.close="});
                    return new DaemonTaskSuccessResult(task);

                case Start:

                    // Start a torrent
                    makeRtorrentCall(log, "d.open", new String[]{task.getTargetTorrent().getUniqueID()});
                    makeRtorrentCall(log, "d.start", new String[]{task.getTargetTorrent().getUniqueID()});
                    return new DaemonTaskSuccessResult(task);

                case StartAll:

                    // Start all torrents
                    makeRtorrentCall(log, "d.multicall2", new String[]{"", "main", "d.open=", "d.start="});
                    return new DaemonTaskSuccessResult(task);

                case SetFilePriorities:

                    // For each of the chosen files belonging to some torrent, set the priority
                    SetFilePriorityTask prioTask = (SetFilePriorityTask) task;
                    String newPriority = "" + convertPriority(prioTask.getNewPriority());
                    // One at a time; rTorrent doesn't seem to support a multicall on a selective number of files
                    for (TorrentFile forFile : prioTask.getForFiles()) {
                        makeRtorrentCall(log, "f.priority.set",
                                new String[]{task.getTargetTorrent().getUniqueID() + ":f" + forFile.getKey(),
                                        newPriority});
                    }
                    return new DaemonTaskSuccessResult(task);

                case SetTransferRates:

                    // Request to set the maximum transfer rates
                    SetTransferRatesTask ratesTask = (SetTransferRatesTask) task;
                    makeRtorrentCall(log, "throttle.global_down.max_rate.set", new String[]{"", (ratesTask.getDownloadRate() == null ? "0" :
                            ratesTask.getDownloadRate().toString() + "k")});
                    makeRtorrentCall(log, "throttle.global_up.max_rate.set", new String[]{"",
                            (ratesTask.getUploadRate() == null ? "0" : ratesTask.getUploadRate().toString() + "k")});
                    return new DaemonTaskSuccessResult(task);

                case SetLabel:

                    SetLabelTask labelTask = (SetLabelTask) task;
                    makeRtorrentCall(log, "d.custom1.set",
                            new String[]{task.getTargetTorrent().getUniqueID(), labelTask.getNewLabel()});
                    return new DaemonTaskSuccessResult(task);

                case ForceRecheck:

                    // Force re-check of data of a torrent
                    makeRtorrentCall(log, "d.check_hash", new String[]{task.getTargetTorrent().getUniqueID()});
                    return new DaemonTaskSuccessResult(task);

                default:
                    return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported,
                            task.getMethod() + " is not supported by " + getType()));
            }
        } catch (DaemonException e) {
            return new DaemonTaskFailureResult(task, e);
        } catch (FileNotFoundException e) {
            return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString()));
        } catch (IOException e) {
            return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.ConnectionError, e.toString()));
        }
    }

    private Object makeRtorrentCall(Log log, String serverMethod, Object[] arguments)
            throws DaemonException, MalformedURLException {

        // Initialise the HTTP client
        if (rpcclient == null) {
            initialise();
        }

        StringBuilder paramsBuilder = new StringBuilder();
        for (Object arg : arguments) {
            paramsBuilder.append(" ").append(arg.toString());
        }
        String params = paramsBuilder.toString();
        String s = params.length() > 100 ? params.substring(0, 100) + "..." : params;
        try {
            log.d(LOG_NAME, "Calling " + serverMethod + " with params [" +
                    s + " ]");
            return rpcclient.call(serverMethod, arguments);
        } catch (IllegalArgumentException e) {
            log.d(LOG_NAME, "Using " + buildWebUIUrl() + ": " + e.toString());
            throw new DaemonException(ExceptionType.ConnectionError, "Error making call to " + serverMethod);
        } catch (XMLRPCException e) {
            log.d(LOG_NAME, e.toString());
            if (e.getCause() instanceof UnauthorizdException) {
                throw new DaemonException(ExceptionType.AuthenticationFailure, e.toString());
            }
            if (e.getCause() instanceof DaemonException) {
                throw (DaemonException) e.getCause();
            }
            throw new DaemonException(ExceptionType.ConnectionError,
                    "Error making call to " + serverMethod + " with params [" +
                            s + " ]: " +
                            e.toString());
        }

    }

    /**
     * Instantiates a XML-RPC client with proper credentials.
     *
     * @throws DaemonException       On conflicting settings (i.e. user authentication but no password or username provided)
     * @throws MalformedURLException Thrown when the URL could not be properly constructed
     */
    private void initialise() throws DaemonException, MalformedURLException {

        int flags = XMLRPCClient.FLAGS_8BYTE_INT;
        this.rpcclient = new XMLRPCClient(HttpHelper.createStandardHttpClient(settings, true), buildWebUIUrl(), flags);

    }

    /**
     * Build the URL of rTorrent's XML-RPC location from the user settings.
     *
     * @return The URL of the RPC API
     */
    private String buildWebUIUrl() {
        String address = settings.getAddress() == null ? "" : settings.getAddress().trim();
        String folder = settings.getFolder() == null ? "" : settings.getFolder().trim();
        return (settings.getSsl() ? "https://" : "http://") + address + ":" + settings.getPort() +
                (TextUtils.isEmpty(folder) ? DEFAULT_RPC_URL : folder);
    }

    private List<Torrent> onTorrentsRetrieved(Object response) throws DaemonException {

        if (!(response instanceof Object[])) {

            throw new DaemonException(ExceptionType.ParsingFailed,
                    "Response on retrieveing torrents did not return a list of objects");

        } else {

            // Parse torrent list from response
            // Formatted as Object[][], see http://libtorrent.rakshasa.no/wiki/RTorrentCommands#Download
            List<Torrent> torrents = new ArrayList<>();
            Map<String, Integer> labels = new HashMap<>();
            Object[] responseList = (Object[]) response;
            for (int i = 0; i < responseList.length; i++) {

                Object[] info = (Object[]) responseList[i];
                String error = (String) info[18];
                error = error.equals("") ? null : error;

                // Determine the time added
                Date added;
                Long addtime = null;
                try {
                    addtime = Long.valueOf(((String) info[19]).trim());
                } catch (NumberFormatException e) {
                    // Not a number (timestamp); ignore and fall back to using creationtime
                }
                if (addtime != null)
                // Successfully received the addtime from rTorrent (which is a String like '1337089336\n')
                {
                    added = new Date(addtime * 1000L);
                } else {
                    // rTorrent didn't have the addtime (missing plugin?): base it on creationtime instead
                    if (info[11] instanceof Long) {
                        added = new Date((Long) info[11] * 1000L);
                    } else {
                        added = new Date((Integer) info[11] * 1000L);
                    }
                }

                // Determine the seeding time
                Date finished = null;
                Long seedingtime = null;
                try {
                    seedingtime = Long.valueOf(((String) info[20]).trim());
                } catch (NumberFormatException e) {
                    // Not a number (timestamp); ignore and fall back to using creationtime
                }
                if (seedingtime != null)
                // Successfully received the seedingtime from rTorrent (which is a String like '1337089336\n')
                {
                    finished = new Date(seedingtime * 1000L);
                }

                // Determine the label
                String label = null;
                try {
                    label = URLDecoder.decode((String) info[21], "UTF-8");
                    if (labels.containsKey(label)) {
                        labels.put(label, labels.get(label) + 1);
                    } else {
                        labels.put(label, 0);
                    }
                } catch (UnsupportedEncodingException e) {
                    // Can't decode label name; ignore it
                }

                String baseFilename = info[17] + "/";

                if (info[3] instanceof Long) {

                    // rTorrent uses the i8 dialect which returns 64-bit integers
                    long rateDownload = (Long) info[3];

                    // @formatter:off
                    torrents.add(new Torrent(
                            i,
                            (String) info[0], // hash
                            (String) info[1], // name
                            convertTorrentStatus((Long) info[2], (Long) info[24], (Long) info[13], (Long) info[14], (Long) info[15]), // status
                            (((Long) info[16]) == 1) ? baseFilename : "", // multi file? base_filename else ""
                            ((Long) info[3]).intValue(), // rateDownload
                            ((Long) info[4]).intValue(), // rateUpload
                            ((Long) info[22]).intValue(), // seedersConnected
                            ((Long) info[5]).intValue() + ((Long) info[6]).intValue(), // seedersKnown
                            ((Long) info[23]).intValue(), // leechersConnected
                            ((Long) info[5]).intValue() + ((Long) info[6]).intValue(), // leechersKnown
                            (rateDownload > 0 ? (int) (((Long) info[12]) / rateDownload) : -1), // eta (bytes left / rate download, if rate > 0)
                            (Long) info[8], // downloadedEver
                            (Long) info[9], // uploadedEver
                            (Long) info[10], // totalSize
                            ((Long) info[8]).floatValue() / ((Long) info[10]).floatValue(), // partDone
                            0f, // TODO: Add availability data
                            label,
                            added,
                            finished,
                            error,
                            settings.getType()));
                    // @formatter:on

                } else {

                    // rTorrent uses the default dialect with 32-bit integers
                    int rateDownload = (Integer) info[3];

                    // @formatter:off
                    torrents.add(new Torrent(
                            i,
                            (String) info[0], // hash
                            (String) info[1], // name
                            convertTorrentStatus(((Integer) info[2]).longValue(), ((Integer) info[24]).longValue(), ((Integer) info[13]).longValue(), ((Integer) info[14]).longValue(), ((Integer) info[15]).longValue()), // status
                            (((Integer) info[16]) == 1) ? baseFilename : "", // multi file? base_filename else ""
                            rateDownload, // rateDownload
                            (Integer) info[4], // rateUpload
                            (Integer) info[22], // seedersConnected
                            (Integer) info[5] + (Integer) info[6], // seedersKnown
                            (Integer) info[23], // leechersConnected
                            (Integer) info[5] + (Integer) info[6], // leechersKnown
                            (rateDownload > 0 ? (Integer) info[12] / rateDownload : -1), // eta (bytes left / rate download, if rate > 0)
                            (Integer) info[8], // downloadedEver
                            (Integer) info[9], // uploadedEver
                            (Integer) info[10], // totalSize
                            ((Integer) info[8]).floatValue() / ((Integer) info[10]).floatValue(), // partDone
                            0f, // TODO: Add availability data
                            label,
                            added,
                            finished,
                            error,
                            settings.getType()));
                    // @formatter:on

                }
            }
            lastKnownLabels = new ArrayList<>();
            for (Entry<String, Integer> pair : labels.entrySet()) {
                if (pair.getKey() != null) {
                    lastKnownLabels.add(new Label(pair.getKey(), pair.getValue()));
                }
            }
            return torrents;

        }

    }

    private List<TorrentFile> onTorrentFilesRetrieved(Object response, Torrent torrent) throws DaemonException {

        if (!(response instanceof Object[])) {

            throw new DaemonException(ExceptionType.ParsingFailed,
                    "Response on retrieveing torrent files did not return a list of objects");

        } else {

            // Parse torrent files from response
            // Formatted as Object[][], see http://libtorrent.rakshasa.no/wiki/RTorrentCommands#Download
            List<TorrentFile> files = new ArrayList<>();
            Object[] responseList = (Object[]) response;
            for (int i = 0; i < responseList.length; i++) {

                Object[] info = (Object[]) responseList[i];
                if (info[1] instanceof Long) {

                    // rTorrent uses the i8 dialect which returns 64-bit integers
                    Long size = (Long) info[1];
                    Long chunksDone = (Long) info[3];
                    Long chunksTotal = (Long) info[4];
                    Long priority = (Long) info[5];

                    // @formatter:off
                    files.add(new TorrentFile(
                            "" + i,
                            (String) info[0], // name
                            torrent.getLocationDir() + info[0], // torrent locationDir + file name
                            (String) info[6], // fullPath
                            size, // size
                            (long) (size * ((float) chunksDone / (float) chunksTotal)), // done
                            convertRtorrentPriority(priority.intValue()))); // priority
                    // @formatter:on

                } else {

                    // rTorrent uses the default dialect with 32-bit integers
                    Integer size = (Integer) info[1];
                    Integer chunksDone = (Integer) info[3];
                    Integer chunksTotal = (Integer) info[4];
                    Integer priority = (Integer) info[5];

                    // @formatter:off
                    files.add(new TorrentFile(
                            "" + i,
                            (String) info[0], // name
                            torrent.getLocationDir() + "/" + info[0], // torrent locationDir + file name
                            (String) info[0], // fullPath
                            size, // size
                            (int) (size * ((float) chunksDone / (float) chunksTotal)), // done
                            convertRtorrentPriority(priority))); // priority
                    // @formatter:on

                }
            }
            return files;

        }

    }

    private Priority convertRtorrentPriority(int code) {
        // Note that Rtorrent has no low priority value
        switch (code) {
            case 0:
                return Priority.Off;
            case 2:
                return Priority.High;
            default:
                return Priority.Normal;
        }
    }

    private int convertPriority(Priority priority) {
        // Note that Rtorrent has no low priority value
        switch (priority) {
            case Off:
                return 0;
            case High:
                return 2;
            default:
                return 1;
        }
    }

    private TorrentStatus convertTorrentStatus(Long state, Long open, Long complete, Long active, Long checking) {
        if (checking == 1) {
            return TorrentStatus.Checking;
        }

        if (open == 1) {
            // links with tracker/peers are open: not stopped
            if (state == 1 && active == 1) {
                if (complete == 1) {
                    return TorrentStatus.Seeding;
                } else {
                    return TorrentStatus.Downloading;
                }
            }
            return TorrentStatus.Paused;
        } else {
            // maybe could be Stopped or Waiting
            return TorrentStatus.Queued;
        }
    }

    private TorrentDetails onTorrentDetailsRetrieved(Log log, Object response) throws DaemonException {

        if (!(response instanceof Object[])) {

            throw new DaemonException(ExceptionType.ParsingFailed,
                    "Response on retrieveing trackers did not return a list of objects");

        } else {

            // Parse a torrent's trackers from response
            // Formatted as Object[][], see http://libtorrent.rakshasa.no/wiki/RTorrentCommands#Download
            List<String> trackers = new ArrayList<>();
            Object[] responseList = (Object[]) response;
            try {
                for (Object aResponseList : responseList) {
                    Object[] info = (Object[]) aResponseList;
                    trackers.add((String) info[0]);
                }
            } catch (Exception e) {
                log.e(LOG_NAME, e.toString());
            }
            return new TorrentDetails(trackers, null);

        }

    }

    @Override
    public Daemon getType() {
        return settings.getType();
    }

    @Override
    public DaemonSettings getSettings() {
        return this.settings;
    }

}
